# Cover Up2date - Exaile plugin
# Copyright (C) 2008 Magnun Leno da Silva <magnun.leno at gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

#############################################################################
# Creator Note
#
# This plugin war written with tehe intention to contribute with this grate
# player adding to it a feature that I think is essencial. It 'syncronize'
# the convers in the ~/.exaile/covers with the Media Library folders.
#
# Obs: This plugin works better if all your library has one folder to each
# album. For compatibility with future features I recomend to keep your
# Media Library like this:
#   $root_folder/artist/album (or)
#   $root_folder/artist/year - album
#
# Currently the syncronization is done in 2 stages:
#  1. Syncronizing all albums by quering the DB and copying the covers
#  2. Every chage of album the plugin check if the cover exist in the 
#     Media Library folders.
#
#############################################################################

# Default plugin things...
import gtk, gobject, time
import xl.plugins as plugins
import xl.path as xlpath
import Image

PLUGIN_NAME = "CoverSync"
PLUGIN_AUTHORS = ['Magnun Leno <magnun.leno@gmail.com>']
PLUGIN_VERSION = '0.4'
PLUGIN_DESCRIPTION = r"""Update the cover in the Folders of the albums in the Media Library\n\nFor bugs or suggestions please visit:\nhttp://coversync.googlecode.com/"""
PLUGIN_ENABLED = False
# TODO: Need to find a better icon....
PLUGIN_ICON = None
CONNS = plugins.SignalContainer()

# Some imports
from os import path
from os import remove
from shutil import copy
from time import sleep
import md5

FILENAMES = ["folder.jpg","Folder.jpg","album.jpg","Album.jpg","albumart.jpg","AlbumArt.jpg","cover.jpg","Cover.jpg"]

# Class to implement the Progress Bar
class ProgressBar:
    '''
            Simple class to create an progress bar to show the Syncronization progress
    '''
    def __init__(self):
        '''Initialize the class'''
        self.title = "Syncronizing Covers"
        self.text = ""
        self.item = 0
        self.total_items = 2112
        self.progress_bar = None
        self.window = None
        self.timer = None
        self.label = None
    def draw(self, pulsing=False):
        '''Draw the widgets'''
        # Simple windows
        self.window = gtk.Window(gtk.WINDOW_POPUP)
        self.window.set_resizable(False)
        #self.window.connect("destroy", self.close_window)
        self.window.set_title(self.title)
        self.window.set_border_width(0)
        self.window.set_position(gtk.WIN_POS_CENTER)
        self.window.set_geometry_hints(min_width=400, 
                                       min_height=70, 
                                       max_width=400, 
                                       max_height=70)
        # Create a VBox
        vbox = gtk.VBox(False, 5)
        vbox.set_border_width(10)
        self.window.add(vbox)
        # Create the desired progress bar
        self.progress_bar = gtk.ProgressBar()
        if pulsing: 
            self.progress_bar.pulse()
        vbox.pack_start(self.progress_bar, False, False, 0)
        # Separator
        separator = gtk.HSeparator()
        vbox.pack_start(separator, False, False, 0)
        # Label
        self.label = gtk.Label()
        self.label.set_label(self.text)
        vbox.pack_start(self.label, False, False, 0)
        # Calls the draw_progress every 5 milisec
        if pulsing:
            self.timer = None
        else:
            self.timer = gobject.timeout_add(5, self.draw_progress, self.progress_bar)
        # Show all widgets
        self.window.show_all()
    def addTimer(self):
        self.timer = gobject.timeout_add(5, self.draw_progress, self.progress_bar)
    def write_text(self, widget = None):
        '''Write the text to the label'''
        self.label.set_label(self.text)

    def draw_progress(self, widget = None):
        '''Update the progress from the progressbar'''
        self.write_text()
        val= (float(self.item) / self.total_items)
        # valid val: 0.0 to 1.0
        self.progress_bar.set_fraction(val)
         # Show percentage
        self.progress_bar.set_text("%.2f%%" % (val * 100))
        return True

    def close_window(self, widget = None):
        '''Close the progress bar'''
        print "Closing..."
        if self.timer:
            gobject.source_remove(self.timer)
        self.window.destroy()
    def pulseme(self, widget = None):
        self.progress_bar.pulse()


# The 'Engine' class...
class CoverSync:
    '''
        This class stores some usefull data to the plugin.
        It has many functions crated to simplify the 'main code' 
        and make it more readable.
        It's __init__ need one argument: path. Path is the fullpath
        to the exaile plugins cover.
    '''
    def __init__(self, path):
        self.pluginPath = path
        # Tracks info
        self.last_track = None
        self.last_root_dir = None
        self.current_track = None
        self.current_root_dir = None
        # Covers info
        self.cover_to_remove = None
        self.cover_path = None
        self.cover_filename = None
        self.cover_root_dir = None
        # Config interface
        self.wTree = None
        self.configDlg = None
        self.chkOverwrite = None
        self.cbeFileName = None
        # Some default configs
        self.filename = "folder.jpg"
        self.saved_config = {}
        self.unsaved_config = {'overwrite':False,
                               'selectedfilename':0} # Also used as a default start up config
    
    def ShowConfig(self):
        '''
            Show the config screen
        '''
        # Load the glade data
        self.wTree = gtk.glade.XML(self.pluginPath+"data/interface.glade", 'configDlg')
        self.configDlg = self.wTree.get_widget('configDlg')
        self.chkOverwrite = self.wTree.get_widget('chkOverwrite')
        self.cbeFileName = self.wTree.get_widget('cbeFileName')
        self.wTree.signal_autoconnect(self)
        
        # Throw saved config into the 'screen'
        self.chkOverwrite.set_active(self.saved_config['overwrite'])
        self.cbeFileName.set_active(self.saved_config['selectedfilename'])
        
        # show the dialog
        self.configDlg.show()
        ret = str(self.configDlg.run())

        # Throw the 'screen' config into the unsaved_config dic
        self.unsaved_config['overwrite'] = self.chkOverwrite.get_active()
        self.filename = self.cbeFileName.get_active_text()
        self.unsaved_config['selectedfilename'] = self.cbeFileName.get_active()
        
        self.configDlg.destroy()
        return int(ret)
    
    def  onSyncNowClick(self, widget):
        '''
            Querries all the existing albums that has the cover 
            in ~/.exaile/covers and copy the covers to the Media
            Library folders, if necessary.
        '''
        # Open Log
        self.openLog("sync")
        self.writeLog("\n############### Starting CoverSync Process ###############")

        # Save unsaved config
        self.unsaved_config['overwrite'] = self.chkOverwrite.get_active()
        self.filename = self.cbeFileName.get_active_text()
        self.unsaved_config['selectedfilename'] = self.cbeFileName.get_active()
        self.SaveConfig()
        self.LoadConfig()
        covers_dir = xlpath.get_config('covers')
        
        # Create ProgressBar in activity mode (pulsing)
        bar= ProgressBar()
        bar.text= "Querying Database..."
        self.writeLog("Querying Database...")
        bar.draw(True)
        bar.write_text()
        bar.pulseme()
        while gtk.events_pending(): 
            gtk.main_iteration()
        sleep(0.5)
        
        # Querying database
        cursor = APP.db.cursor()
        cursor.execute("""SELECT DISTINCT(albums.id), albums.image, paths.name 
                          FROM albums, tracks, paths, artists
                          WHERE tracks.album=albums.id AND tracks.path=paths.id AND tracks.artist=artists.id
                          GROUP BY albums.id 
                          ORDER BY artists.name""")
                          
        # Fetching
        self.writeLog("Fetching cursor...")
        bar.text = "Fetching cursor..."
        bar.write_text()
        bar.pulseme()
        while gtk.events_pending(): 
            gtk.main_iteration()
        covers = []
        
        # Check if can overwrite
        if self.saved_config['overwrite']:      
            # Overwrite loop
            for entry in cursor:
                if not (entry[1] in ["",u"nocover",None]) and path.isdir(path.split(entry[2])[0]):
                    covers.append([covers_dir+"/"+entry[1],path.split(entry[2])[0]+"/"+self.filename])
                    self.writeLog("[ADD] "+`covers[-1]`)
                    bar.pulseme()
                while gtk.events_pending(): 
                    gtk.main_iteration()
        else:
            # Not overwrite loop
            for entry in cursor:
                if not (entry[1] in ["",u"nocover",None]) and path.isdir(path.split(entry[2])[0]):
                    # Already exists
                    if path.exists(path.split(entry[2])[0]+"/"+self.filename):
                        self.writeLog("[SKP] "+path.split(entry[2])[0]+"/"+self.filename+" EXISTS!!!")
                    else:
                        covers.append([covers_dir+"/"+entry[1],path.split(entry[2])[0]+"/"+self.filename])
                        self.writeLog("[ADD] "+`covers[-1]`)
                        bar.pulseme()
                while gtk.events_pending(): 
                    gtk.main_iteration()
        del cursor
        time.sleep(0.5)
        
        # Check if there is any cover to sync
        if len(covers) is 0:
            bar.close_window()
            self.ShowMessage("No covers to syncronize...")
            self.writeLog("No covers to syncronize...")
            self.closeLog()
            return
            
        # Starting sync
        bar.progress_bar.set_fraction(0)
        self.writeLog("\n\nStarting syncronization...")
        bar.text = "Starting syncronization..."
        bar.write_text()
        while gtk.events_pending(): 
            gtk.main_iteration()
        time.sleep(0.5)
        
        bar.addTimer()
        bar.item = 0
        bar.total_items = len(covers)
        for i in range(bar.total_items):
            bar.item += 1
            bar.text = "Syncronizing covers... "+`bar.item`+"/"+`bar.total_items`
            self.writeLog("["+`bar.item`+"] SRC: "+covers[i][0] +"\tDST: "+covers[i][1])
            try:
                img = Image.open(covers[i][0])
                if img.size[0] != img.size[1]:
                    less = img.size[0]
                    if img.size[0] > img.size[1]:
                        less = img.size[1]
                    img = img.crop((0,0,less,less))
                img = img.resize((300,300))
                img.convert('RGB').save(covers[i][1],"JPEG")
            except:
                self.writeLog("[ERR] SRC: "+covers[i][0] +"\tDST: "+covers[i][1])
            while gtk.events_pending(): 
                gtk.main_iteration()
        
        bar.progress_bar.set_fraction(0)
        bar.progress_bar.set_text("100%")
        while gtk.events_pending(): 
            gtk.main_iteration()
        time.sleep(2)
        
        bar.close_window()
        self.closeLog()
        
        ShowMessage("Syncronized a total of "+`bar.total_items`+" covers.\nThe syncronization log is at CoverSync plugin folder.")
        
    def onCleanFoldersClick(self, widget):
        '''
            Remove every cover in albums folders witch has the names in
            FILENAMES
        '''
        # Open Log
        self.openLog("clean")
        self.writeLog("\n############### Starting CleanFolders Process ###############")
        
        # Create ProgressBar in activity mode (pulsing)
        bar= ProgressBar()
        bar.text= "Querying Database..."
        self.writeLog("Querying Database...")
        bar.draw(True)
        bar.write_text()
        bar.pulseme()
        while gtk.events_pending(): 
            gtk.main_iteration()
        sleep(0.5)
        
        # Querying database
        cursor = APP.db.cursor()
        cursor.execute("""SELECT DISTINCT(albums.id), artists.name, paths.name
                          FROM albums, tracks, paths, artists
                          WHERE tracks.album=albums.id AND tracks.path=paths.id AND tracks.artist=artists.id
                          GROUP BY albums.id 
                          ORDER BY artists.name""")
                          
        # Fetching
        self.writeLog("Fetching cursor...")
        bar.text = "Fetching cursor..."
        bar.write_text()
        bar.pulseme()
        while gtk.events_pending(): 
            gtk.main_iteration()
        albums = cursor.fetchall()
        del cursor
        sleep(0.5)
        
        # Creating List of files to remove
        bar.text = "Creating list of files..."
        bar.write_text()
        self.writeLog("Creating list of files...")
        files = []
        for album in albums:
            bar.pulseme()
            while gtk.events_pending(): 
                gtk.main_iteration()
            for name in FILENAMES:
                if path.exists(path.split(album[2])[0]+"/"+name):
                    files.append([ path.split(album[2])[0]+"/"+name, 
                                   album[1] ])
        del albums
        sleep(0.5)
        
        # Check if there is any cover to remove
        if len(files) is 0:
            self.writeLog("No covers to remove...")
            bar.text= "No covers to remove..."
            bar.write_text()
            bar.progress_bar.set_fraction(1)
            while gtk.events_pending(): 
                gtk.main_iteration()
            sleep(2)
            self.closeLog()
            bar.close_window()
            return
        
        # Preparing to remove
        bar.progress_bar.set_fraction(0)
        self.writeLog("Preparing to remove")
        bar.text= "Preparing to remove"
        bar.write_text()
        while gtk.events_pending(): 
            gtk.main_iteration()
        sleep(1)
       
        # Remove loop
        bar.addTimer()
        bar.item = 0
        bar.total_items = len(files)
        for i in range(bar.total_items):
            bar.text= "Checking "+files[i][1]+" albums..."
            if path.exists(files[i][0]):
                self.writeLog("["+`bar.item+1`+"] deleting "+files[i][0])
                remove(files[i][0])
            else:
                self.writeLog("["+`bar.item+1`+"] deleting "+files[i][0]+" DUP!!")
            bar.item+= 1
            while gtk.events_pending(): 
                gtk.main_iteration()
        
        # Ending process
        bar.progress_bar.set_fraction(1)
        bar.progress_bar.set_text("100%")
        bar.text= "Deleted "+`bar.total_items`+" covers..."
        bar.write_text()
        while gtk.events_pending(): 
            gtk.main_iteration()
        sleep(2)
        bar.close_window()
        self.closeLog()
        ShowMessage("deleted a total of "+`bar.total_items`+" covers.\nThe cleaning log is at CoverSync plugin folder.")
    
    def ShowMessage(self, text):
        '''
            A simple message box used to create some output. 
            Used during the development, may be usefull in future...
        '''
        dialog = gtk.MessageDialog(APP.window,
                                   gtk.DIALOG_DESTROY_WITH_PARENT,
                                   gtk.MESSAGE_INFO,
                                   gtk.BUTTONS_OK,
                                   text)
        dialog.run()
        dialog.destroy()
    def openLog(self, type):
        self.log = None
        if type is "clean":
            self.log = open(self.pluginPath+"/clean.log","w")
        elif type is "sync":
            self.log = open(self.pluginPath+"/sync.log","w")
        self.log.write("Log file: "+time.asctime()+"\n\n")
        
    def closeLog(self):
        self.log.write("\n######################## End of Log ########################\n")
        self.log.close()
        self.log = None
        
    def writeLog(self, text):
        self.log.write(text+"\n")
    
    def GetRootDir(self, track):
        '''
            Receives the track info and return the root dir without 
            the slash.
        '''
        return path.split(track.get_loc_for_io())[0]
    def GetDesiredPath(self):
        '''
            Return the fullpath of the desired destiny.
        '''
        return self.current_root_dir+'/'+self.filename
    def SetCurrentTrack(self, track):
        '''
            Receive the current track and store some usefull data
            based in the track. It also keep track from the last track
        '''
        # Math: track[n-1] = track[n]
        self.last_track = self.current_track
        self.last_root_dir = self.current_root_dir
        # Save current track
        self.current_track = track
        self.current_root_dir = self.GetRootDir(self.current_track)
    def SetCurrentCover(self, cover):
        '''
            Receives the current cover, split the filename and path 
            and store it for further use
        '''
        # Saves fullpath
        self.cover_path = cover
        # Split fullpath
        (self.cover_root_dir, self.cover_filename) = path.split(self.cover_path)
    def CopyCover(self):
        '''
            Copy the cover from it's origin to the correct path
        '''
        # Open Image
        img = Image.open(self.cover_path)
        # If image not a square
        if img.size[0] != img.size[1]:
            # Discover the smaller side
            less = img.size[0]
            if img.size[0] > img.size[1]:
                less = img.size[1]
            # Crop with a square (lessXless)
            img = img.crop((0,0,less,less))
        # Resize to the default size
        img = img.resize((300,300))
        # Convert to RGB in case of a true-color image and save as .jpg
        img.convert('RGB').save(self.GetDesiredPath(),"JPEG")
    def RenameCover(self):
        '''
            Copy the cover using CopyCover and append it to be removed later
        '''
        self.CopyCover()
        self.cover_to_remove = self.cover_path
    def RemoveLastCover(self):
        '''
            Remove the unused cover
        '''
        remove(self.cover_to_remove)
        self.cover_to_remove = None
    def isTracksDiffAlbum(self):
        '''
            Compares if the playing track and the last track are from 
            different albums
        '''
        return (self.last_track == None or self.last_track.album != self.current_track.album)
    def isTracksSameRoot(self):
        '''
            Compares if the current track and the last track are int the 
            same root folder
        '''
        return (self.last_root_dir == self.current_root_dir)
    def isDBCoverAtLib(self):
        '''
            CHeck if the cover file is already at the Media Library Folder with the correct filename
        '''
        return (self.current_root_dir+'/'+self.filename == self.cover_path)
    def isDBCoverAtLibDir(self):
        '''
            CHeck if the cover file is already at the Media Library Folders
        '''
        return (self.current_root_dir == self.cover_root_dir)
    def Digest(self, file_path):
        '''
            Hashes the indicated file and return it's digest
        '''
        return md5.new(open(file_path).read()).digest()
        
    def LoadConfig(self):
        '''
            Load the configs
        '''
        # Clear saved_config dic
        self.saved_config.clear()
        
        # Load configs
        self.saved_config['overwrite'] = APP.settings.get_boolean('overwrite', default=False, plugin=plugins.name(__file__))
        self.saved_config['selectedfilename'] = APP.settings.get_int('selectedfilename', default=0, plugin=plugins.name(__file__))
        self.filename = FILENAMES[self.saved_config['selectedfilename']]
        # Make a mirror in unsaved configs
        self.unsaved_config = self.saved_config.copy()
    
    def SaveConfig(self):
        '''
            Save the configs
        '''
        APP.settings.set_boolean('overwrite', self.unsaved_config['overwrite'], plugin=plugins.name(__file__))
        APP.settings.set_int('selectedfilename', self.unsaved_config['selectedfilename'], plugin=plugins.name(__file__))
        

# create an global instance for the CoverSync class
yyz = CoverSync(xlpath.get_config('plugins')+"/coversync/")

# I'm a little lazy. So let's do some shortcuts...
ShowMessage = yyz.ShowMessage

# Now, Roll the Bones...
def initialize():
    '''
        Initializing plugin...
    '''
    CONNS.connect(APP.player, 'play-track', play_track)
    CONNS.connect(APP.cover, 'image-changed', lambda w, c: changeCover(c))
    yyz.LoadConfig()
    return True

def destroy():
    '''
        Called when the plugin is disabled
    '''
    CONNS.disconnect_all()
    
def configure():
    '''
        Configure in the Plugin Manager.
        It show the config windows and saves if clicked OK
    '''
    yyz.LoadConfig()
    if yyz.ShowConfig():
        yyz.SaveConfig()
        yyz.LoadConfig()

def play_track(exaile, track):
    '''
        Called when a track starts
    '''
    # saves a 'history' of the played tracks
    yyz.SetCurrentTrack(track)
    
def changeCover(cover):
    '''
        Called when the cover changes. 
        It's called even when changes to 'nocover'
    '''
    # Some filter here. It avoid the 'nocover'
    if 'nocover' in cover:
        return
    if cover is None:
        return
    # Now, a little more filters to avoid unecessary process
    # Library path changed??
    if not yyz.isTracksDiffAlbum(): return # Nope
    # Check if any cover has been renamed before, and remove the unused
    if yyz.cover_to_remove != None:
        yyz.RemoveLastCover()
    
    # Update cover info in the class
    yyz.SetCurrentCover(cover)
    # Is the cover already at the Media Library??
    if yyz.isDBCoverAtLib(): return # Everything's fine
    
    # Is the cover at the same dir of the playing track?
    if yyz.isDBCoverAtLibDir(): 
        # It is in the same folder of the album. It may be with the wrong filename, let's rename
        yyz.RenameCover()
        return
    # So,  it isn't in the Library. But maybe an cover already exist there!
    if path.exists(yyz.GetDesiredPath()):
        # Does the user want us to overwrite the existing one?!
        if not yyz.saved_config['overwrite']: return # Nope
        # Ok, the user want us to overwrite. Let's check if the cover isn't the same as the one in the ~/.exaile/covers
        if yyz.Digest(yyz.cover_path) != yyz.Digest(yyz.GetDesiredPath()):
            # Nope! Let's update...
            yyz.CopyCover()
        return
    yyz.CopyCover()
    
